package agents.client;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.FSMBehaviour;
import jade.core.behaviours.OneShotBehaviour;
import jade.core.behaviours.SimpleBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.domain.FIPAException;
import jade.lang.acl.ACLMessage;
import agents.manager.ManagerAgent;
import jade.lang.acl.MessageTemplate;
import javax.swing.JOptionPane;
import org.joda.time.DateTime;

/**
 * This class represents a Client that interact with a Manager Agent and during
 * the conversation, they change messages that allows establish in order to
 * produce components.
 * 
 * @author Group 6
 */
public class ClientAgent extends Agent {
    
    // Interface Gráfico
    private ClientAgentGUI gui;
    // Guarda o AID do manager para que a MachineAgent trabalha
    private AID managerAID;
    // Número de componentes pretendidos para a encomenda
    private int qty;
    // Preço mínimo a pagar por componente
    private int minPrice;
    // Preço máximo a pagar por componente
    private int maxPrice;
    // Preço acordado para a encomenda
    private int agreedPrice;
    // Deadline para a data de entrega
    private DateTime deadline;
    // Controlo da negociação
    private boolean retryNegociation;
    // Controlo do botão "Add Order"
    private boolean buttonAddOrderPressed;
    
    // FSM - Identificação dos diferentes estados, constantes associadas e
    // variáveis associadas ao seu funcionamento
    private static final String FSM_SEARCH_MANAGER = "Search Manager";
    private static final int FSM_SEARCH_MANAGER_ATTEMPTS = 3;
    private static final int FSM_SEARCH_MANAGER_TBATTEMPTS = 5000;

    private static final String FSM_CHECK_EXISTING_ORDER = "Check Existing Order";
    private static final String FSM_CHECK_EXISTING_ORDER_ASK = FSM_CHECK_EXISTING_ORDER + " Ask";
    private static final String FSM_CHECK_EXISTING_ORDER_ANSWER = FSM_CHECK_EXISTING_ORDER + " Answer";
    
    private static final String FSM_ORDER_REQUEST = "Order Request";
    private static final String FSM_ORDER_REQUEST_ASK = FSM_ORDER_REQUEST + " Ask";
    private static final String FSM_ORDER_REQUEST_ANSWER = FSM_ORDER_REQUEST + " Answer";
    
    private static final String FSM_CLOSE = "Close";

    public ClientAgent() {
        /*
         * Devido às especificidades da classe Agent a inicialização da classe
         * dá-se no método setup e respectivos subordinados.
         */
    }
    
    @Override
    protected void setup() {
        System.out.println("-> Agente " + this.getLocalName() + " -> Ligado!");
        super.setup();
        // Definir características da MachineAgent
        this.initAllSettings();
        // Construir e adicionar o FSM Behavior que lida com todo o funcionamento do agente
        this.buildAndAddFSMBehavior();
    }
    
    private void initAllSettings() {
        // Inicialmente, o AID da manager não se encontra definido
        this.managerAID = null;
        // Apresentar GUI e 1ª inicialização dos dados
        this.gui = new ClientAgentGUI(this);
        // O cliente tem que indicar posteriormente os dados da encomenda
        this.qty = -1;
        this.minPrice = -1;
        this.maxPrice = -1;
        this.agreedPrice = -1;
        this.deadline = null;
        this.buttonAddOrderPressed = false;
        this.retryNegociation = false;
    }
    
    private void buildAndAddFSMBehavior() {
        // FSM - Criar instância
        FSMBehaviour fsm = new FSMBehaviour();
        // FSM - Primeiro estado
        fsm.registerFirstState(new SearchManager(this, FSM_SEARCH_MANAGER_TBATTEMPTS), FSM_SEARCH_MANAGER);
        // FSM - Estados intermédios
        fsm.registerState(new CheckExistingOrderAsk(), FSM_CHECK_EXISTING_ORDER_ASK);
        fsm.registerState(new CheckExistingOrderAnswer(), FSM_CHECK_EXISTING_ORDER_ANSWER);
        fsm.registerState(new OrderRequestAsk(), FSM_ORDER_REQUEST_ASK);
        fsm.registerState(new OrderRequestAnswer(), FSM_ORDER_REQUEST_ANSWER);
        // FSM - Último estado
        fsm.registerLastState(new Close(), FSM_CLOSE);
        // FSM - Definição das transições
        fsm.registerTransition(FSM_SEARCH_MANAGER, FSM_CLOSE, 0);
        fsm.registerTransition(FSM_SEARCH_MANAGER, FSM_CHECK_EXISTING_ORDER_ASK, 1);
        
        fsm.registerTransition(FSM_CHECK_EXISTING_ORDER_ASK, FSM_CLOSE, 0);
        fsm.registerTransition(FSM_CHECK_EXISTING_ORDER_ASK, FSM_CHECK_EXISTING_ORDER_ANSWER, 1);
        
        fsm.registerTransition(FSM_CHECK_EXISTING_ORDER_ANSWER, FSM_CLOSE, 0);
        fsm.registerTransition(FSM_CHECK_EXISTING_ORDER_ANSWER, FSM_CHECK_EXISTING_ORDER_ASK, 1);
        fsm.registerTransition(FSM_CHECK_EXISTING_ORDER_ANSWER, FSM_ORDER_REQUEST_ASK, 2);
        
        fsm.registerTransition(FSM_ORDER_REQUEST_ASK, FSM_CLOSE, 0);
        fsm.registerTransition(FSM_ORDER_REQUEST_ASK, FSM_ORDER_REQUEST_ASK, 1);
        fsm.registerTransition(FSM_ORDER_REQUEST_ASK, FSM_ORDER_REQUEST_ANSWER, 2);
        
        fsm.registerTransition(FSM_ORDER_REQUEST_ANSWER, FSM_CLOSE, 0);
        fsm.registerTransition(FSM_ORDER_REQUEST_ANSWER, FSM_ORDER_REQUEST_ASK, 1);
        fsm.registerTransition(FSM_ORDER_REQUEST_ANSWER, FSM_CHECK_EXISTING_ORDER_ASK, 2);
        
        // Adicionar o comportamento FSM ao agente
        addBehaviour(fsm);
    }
    
    /**
     * A classe SearchManager, baseada num TickerBehaviour, procura pelo agente
     * encarregue de oferecer serviços de produção de componentes.
     */
    private class SearchManager extends TickerBehaviour {

        public SearchManager(Agent a, long period) {
            super(a, period);
        }

        private void initSettings() {
            gui.setClientStatus("Searching Manager");
        }

        @Override
        public void onTick() {
            this.initSettings();
            if (getTickCount() < FSM_SEARCH_MANAGER_ATTEMPTS) {
                DFAgentDescription template = new DFAgentDescription();
                ServiceDescription sd = new ServiceDescription();
                sd.setType(ManagerAgent.SD_ORDERSERVICE_TYPE);
                sd.setName(ManagerAgent.SD_ORDERSERVICE_NAME);
                template.addServices(sd);
                try {
                    DFAgentDescription searchResult[] = DFService.search(myAgent, template);
                    for (int i = 0; i < searchResult.length; i++) {
                        managerAID = searchResult[i].getName();
                        gui.appendInfoToClientLog(FSM_SEARCH_MANAGER + " - Found Manager: " + managerAID.getLocalName());
                        stop();
                    }
                } catch(FIPAException ex) {
                    ex.printStackTrace();
                }
            } else {
                int answer = JOptionPane.showConfirmDialog(
                        gui,
                        "The attempt to communicate with the Manager failed " + FSM_SEARCH_MANAGER_ATTEMPTS
                        + " times. Should I try again?",
                        "Question",
                        JOptionPane.YES_NO_OPTION);
                if (answer == 0) {
                    // Se sim
                    reset();
                } else {
                    stop();
                }
            }
        }

        @Override
        public int onEnd() {
            return (managerAID == null) ? 0 : 1;
        }
        
    }
    
    private class CheckExistingOrderAsk extends OneShotBehaviour {
        
        private void initSettings() {
            gui.setClientStatus("Check Existing Order");
        }
        
        @Override
        public void action() {
            this.initSettings();
            // Pergunta ao manager se tem encomendas do cliente
            ACLMessage msg = new ACLMessage(ACLMessage.QUERY_IF);
            msg.addReceiver(managerAID);
            msg.setConversationId(ManagerAgent.MSG_CHECK_EXISTING_ORDER);
            myAgent.send(msg);
        }

        @Override
        public int onEnd() {
            return 1;
        }
        
    }
    
    private class CheckExistingOrderAnswer extends SimpleBehaviour {
        
        private boolean receivedAnswer;
        private boolean orderPending;
        private boolean orderFinished;
        
        private void initSettings() {
            gui.setClientStatus("Check Existing Order");
            this.receivedAnswer = false;
            this.orderPending = false;
            this.orderFinished = false;
        }
        
        @Override
        public void action() {
            initSettings();
            // Aguarda por resposta do manager
            MessageTemplate mt = MessageTemplate.and(
                    MessageTemplate.MatchConversationId(ManagerAgent.MSG_CHECK_EXISTING_ORDER),
                    MessageTemplate.MatchPerformative(ACLMessage.INFORM_IF));
            ACLMessage replyMsg = myAgent.receive(mt);
            if (replyMsg != null) {
                receivedAnswer = true;
                // Analisar e processar conteúdo da mensagem
                String[] msgArray = replyMsg.getContent().split(";");
                if (msgArray.length == 1) {
                    // Não existem encomendas definidas pelo cliente
                    gui.appendInfoToClientLog(FSM_CHECK_EXISTING_ORDER + " - There is no order!");
                } else if (msgArray.length == 5) {
                    // Existe uma encomenda finalizada
                    this.orderFinished = true;
                    qty = Integer.parseInt(msgArray[2]);
                    deadline = DateTime.parse(msgArray[4]);
                    // Se foi recebida dentro do prazo acordado
                    if (deadline.isAfterNow()) {
                        agreedPrice = Integer.parseInt(msgArray[3]);
                        gui.setOrderDefined(true, qty, deadline);
                        gui.setOrderStatus("Finished", 100, "Price: " + agreedPrice + " / Within Deadline");
                        gui.appendInfoToClientLog(FSM_CHECK_EXISTING_ORDER + " - The order is finished!");
                    } else {
                        agreedPrice = Integer.parseInt(msgArray[3]);
                        gui.setOrderDefined(true, qty, deadline);
                        gui.setOrderStatus("Finished", 100, "Initial Price: " + agreedPrice
                                + " / Final Price: " + ManagerAgent.COMPONENT_MIN_PRICE_LATE
                                + " (After the Deadline)");
                        gui.appendInfoToClientLog(FSM_CHECK_EXISTING_ORDER + " - The order is finished!");
                    }
                } else if (msgArray.length == 6) {
                    // Existe uma encomenda pendente
                    this.orderPending = true;
                    qty = Integer.parseInt(msgArray[3]);
                    deadline = DateTime.parse(msgArray[5]);
                    agreedPrice = Integer.parseInt(msgArray[4]);
                    int done = Integer.parseInt(msgArray[2]);
                    gui.setOrderDefined(true, qty, deadline);
                    gui.setOrderStatus("Pending", done, "Price: " + agreedPrice);
                    gui.appendInfoToClientLog(FSM_CHECK_EXISTING_ORDER + " - The order is pending!");
                }
            } else {
                block();
            }
            doWait(3000);
        }

        @Override
        public boolean done() {
            return receivedAnswer;
        }
        
        @Override
        public int onEnd() {
            if (this.orderFinished) {
                JOptionPane.showMessageDialog(gui,
                        "Order Finished and Received! Check Order Status Area for more info!", "Alert!", JOptionPane.INFORMATION_MESSAGE);
                return 1;
            } else if (this.orderPending) {
                return 1;
            } else {
                int answer = JOptionPane.showConfirmDialog(
                        gui,
                        "You do not have a defined order. You want to order?",
                        "Question",
                        JOptionPane.YES_NO_OPTION);
                if (answer == 0) {
                    // Se sim
                    return 2;
                } else {
                    return 0;
                }
            }
        }
        
    }
    
    private class OrderRequestAsk extends OneShotBehaviour {

        private boolean orderFilled;
        
        private void initSettings() {
            gui.setClientStatus("Requesting Order");
            gui.enableUserInput();
            this.orderFilled = false;
        }

        @Override
        public void action() {
            initSettings();
            if ((buttonAddOrderPressed == true && minPrice != 0 && maxPrice != 0 && qty != 0 && deadline != null)
                    || retryNegociation) {
                buttonAddOrderPressed = false;
                this.orderFilled = true;
                String msgContent = minPrice + ";" + qty + ";" + deadline;
                ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
                msg.addReceiver(managerAID);
                msg.setContent(msgContent);
                msg.setConversationId(ManagerAgent.MSG_ORDER_REQUEST);
                gui.appendInfoToClientLog(FSM_ORDER_REQUEST + " - Request Sent!");
                //System.out.println("Client tenta efectuar encomenda!");
                myAgent.send(msg);
                //JOptionPane.showMessageDialog(gui, msgContent, "Tentativa Encomenda", JOptionPane.PLAIN_MESSAGE);
            }
        }

        @Override
        public int onEnd() {
            return (this.orderFilled) ? 2 : 1;
        }
        
    }
    
    private class OrderRequestAnswer extends SimpleBehaviour {

        private boolean responseReceived;
        private boolean orderAccepted;
        private boolean orderAlmostAccepted;
        private boolean orderRefused;
        private boolean orderRequestCompletelyBroken;
        private String obs;

        private void initSettings() {
            gui.setClientStatus("Requesting Order");
            gui.disableUserInput();
            this.responseReceived = false;
            this.orderAccepted = false;
            this.orderAlmostAccepted = false;
            this.orderRefused = false;
            this.orderRequestCompletelyBroken = false;
            this.obs = "";
        }
        
        @Override
        public void action() {
            this.initSettings();
            MessageTemplate mt = MessageTemplate.MatchConversationId(ManagerAgent.MSG_ORDER_REQUEST);
            ACLMessage responseMsg = myAgent.receive(mt);
            if (responseMsg != null) {
                this.responseReceived = true;
                if (responseMsg.getPerformative() == ACLMessage.AGREE) {
                    this.orderAccepted = true;
                } else if (responseMsg.getPerformative() == ACLMessage.PROPOSE) {
                    this.orderAlmostAccepted = true;
                    this.analyseProposeOrRefuseResponseMsg(responseMsg.getContent());
                } else if (responseMsg.getPerformative() == ACLMessage.REFUSE) {
                    this.orderRefused = true;
                    this.analyseProposeOrRefuseResponseMsg(responseMsg.getContent());
                } 
            } else {
                block();
            }
        }
        
        private void analyseProposeOrRefuseResponseMsg(String responseMsg) {
            // Tratar mensagem
            String deadlineStatus = responseMsg.split(";")[0];
            String priceOffered = responseMsg.split(";")[1];
            // Se a ordem foi rejeitada
            if (this.orderRefused) {
                // Verificar o estado da Deadline
                if (deadlineStatus.equalsIgnoreCase("IMPOSSIBLE")) {
                    // Não é possível produzir a encomenda, dentro do prazo pretendido
                    this.orderRequestCompletelyBroken = true;
                    this.obs = "Deadline Too Short";
                } else if (deadlineStatus.equalsIgnoreCase("OVERRIDE")) {
                    // É possível produzir a encomenda (atrasando outras) mas, é necessário pagar, no mínimo mais 50%
                    // Verificar se existe margem para aumentar 50% ao preço mínimo disposto a pagar
                    int inflatedPrice = (int) (minPrice * 1.5F);
                    if (inflatedPrice > maxPrice) {
                        // Não existe margem, não é possível produzir a encomenda
                        this.orderRequestCompletelyBroken = true;
                        this.obs = "Order Deadline overrides another order! Price too low!";
                    } else {
                        minPrice = inflatedPrice;
                    }
                } else if (deadlineStatus.equalsIgnoreCase("OK")) {
                    // É possível produzir a encomenda mas, é necessário pagar, no mínimo mais 20%
                    int inflatedPrice = (int) (minPrice * 1.2F);
                    if (inflatedPrice > maxPrice) {
                        // Não existe margem, não é possível produzir a encomenda
                        this.orderRequestCompletelyBroken = true;
                        this.obs = "Price too low!";
                    } else {
                        minPrice = inflatedPrice;
                    }
                }
            }
            // Se foi feita uma contra-proposta, é necessário pagar, no mínimo mais entre 1 a 20%
            if (this.orderAlmostAccepted) {
                int inflatedPrice = (int) (minPrice * 1.05F);
                if (inflatedPrice > maxPrice) {
                    // Não existe margem, não é possível produzir a encomenda
                    this.orderRequestCompletelyBroken = true;
                    this.obs = "Price too low!";
                } else {
                    minPrice = inflatedPrice;
                }
            }
        }

        @Override
        public boolean done() {
            return this.responseReceived;
        }

        @Override
        public int onEnd() {
            if (orderRequestCompletelyBroken) {
                JOptionPane.showMessageDialog(gui, obs, "Order Request Status",
                        JOptionPane.ERROR_MESSAGE);
                retryNegociation = false;
                return 2;
            } else if (orderAccepted) {
                JOptionPane.showMessageDialog(gui, "Accepted", "Order Request Status",
                        JOptionPane.INFORMATION_MESSAGE);
                retryNegociation = false;
                return 2;
            } else if (orderAlmostAccepted || orderRefused) {
                //JOptionPane.showMessageDialog(gui, obs, "Order Request Failed",
                //        JOptionPane.WARNING_MESSAGE);
                retryNegociation = true;
                return 1;
            } else {
                return 0;
            }
        }
        
    }
    
    /**
     * A classe Close, baseada em OneShotBehaviour, tem como função assegurar
     * que no momento de encerramento do agente Client, todas as funcionalidades
     * e interface gráfico é fechado devidamente.
     */
    private class Close extends OneShotBehaviour {

        private void initSettings() {
            gui.setClientStatus("Closing Client");
        }
        
        @Override
        public void action() {
            this.initSettings();
            shutdownAgentAndGUI();
        }
        
    }
    
    
    
    /*
    private class WaitForOrderDelivery extends SimpleBehaviour {

        private boolean deliveryReceived;

        private void initData() {
            this.deliveryReceived = false;
        }
        
        @Override
        public void action() {
            this.initData();
            MessageTemplate mt = MessageTemplate.or(
                    MessageTemplate.MatchPerformative(ACLMessage.INFORM),
                    MessageTemplate.MatchPerformative(ACLMessage.CANCEL));
            mt = MessageTemplate.and(
                    mt,
                    MessageTemplate.MatchConversationId(ManagerAgent.MSG_ORDER_DELIVERY));
            ACLMessage responseMsg = myAgent.receive(mt);
            if (responseMsg != null) {
                if (responseMsg.getPerformative() == ACLMessage.INFORM) {
                    this.deliveryReceived = true;
                    gui.appendInfoToClientLog(FSM_WAIT_FOR_ORDER_DELIVERY + " - SUCCESS! All products received!");
                } else if (responseMsg.getPerformative() == ACLMessage.CANCEL) {
                    this.deliveryReceived = false;
                    gui.appendInfoToClientLog(FSM_WAIT_FOR_ORDER_DELIVERY + " - Failure! Order Canceled by Manager!");
                }
            } else {
                gui.appendInfoToClientLog(FSM_WAIT_FOR_ORDER_DELIVERY + " - Waiting for order processing!");
                block();
            }
        }

        @Override
        public boolean done() {
            return deliveryReceived;
        }

        @Override
        public int onEnd() {
            return 0;
        }
        
    }
    */
    
    
    
    /**
     * Permite que a GUI insira os dados de uma nova encomenda no conhecimento
     * do agente.
     * @param minPriceGui preço mínimo por unidade
     * @param maxPriceGui preço máximo por unidade
     * @param productQtyGui número de unidades a encomendar
     * @param deadlineGui prazo de entrega
     * @return sucesso da operação
     */
    public boolean updateOrder(final int minPriceGui, final int maxPriceGui, final int productQtyGui,
            final DateTime deadlineGui) {
        addBehaviour(new OneShotBehaviour() {
            public void action() {
                minPrice = minPriceGui;
                maxPrice = maxPriceGui;
                qty = productQtyGui;
                deadline = deadlineGui;
                buttonAddOrderPressed = true;
            }
        });
        return true;
    }
    
    /**
     * Elimina a interface gráfica e o próprio agente.
     */
    public void shutdownAgentAndGUI() {
        System.out.println("-> Agente " + this.getLocalName() + " -> Desligado!");
        this.gui.setVisible(false);
        this.gui.dispose();
        this.gui = null;
        this.doDelete();
    }
    
}